package beanstao.util.io.chars;

import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import beanstao.util.Util;
import beanstao.util.collections.Context;
import beanstao.util.collections.NutMap;
import beanstao.util.reflect.Mirror;

public class CharSegment implements ISegment, Cloneable
{

	public CharSegment()
	{
	}

	public CharSegment(String str)
	{
		this.valueOf(str);
	}

	@SuppressWarnings("unchecked")
	public ISegment add(String key, Object v)
	{
		if (!this.context.has(key))
		{
			this.context.set(key, v);
			return this;
		}
		Object val = this.context.get(key);
		if (val == null)
		{
			this.context.set(key, v);
		}
		else if (val instanceof Collection<?>)
		{
			((Collection<Object>) val).add(v);
		}
		else
		{
			List<Object> objSet = new LinkedList<Object>();
			objSet.add(val);
			objSet.add(v);
			this.context.set(key, objSet);
		}
		return this;
	}

	public void clearAll()
	{
		this.context.clear();
	}

	public boolean contains(String key)
	{
		return this.keys.containsKey(key);
	}

	public ISegment born()
	{
		return new CharSegment(this.getOrginalString());
	}

	private String orgString;

	public String getOrginalString()
	{
		return this.orgString;
	}

	@Override
	public ISegment clone()
	{
		CharSegment cs = new CharSegment();
		cs.parse(new StringReader(this.orgString));
		cs.context = this.context.clone();
		return cs;
	}

	public Set<String> keys()
	{
		return this.keys.keySet();
	}

	public List<Object> values()
	{
		List<Object> re = new ArrayList<Object>(this.nodes.size());
		for (Node node : this.nodes)
		{
			if (node.isKey)
			{
				re.add(this.context.get(node.value));
			}
			else
			{
				re.add(node.value);
			}
		}
		return re;
	}

	public ISegment setAll(Object v)
	{
		for (String key : this.keys())
		{
			this.context.set(key, v);
		}
		return this;
	}

	public ISegment setBy(Object obj)
	{
		Iterator<String> it = this.keys().iterator();
		Class<?> klass = obj.getClass();
		Mirror<?> mirror = Mirror.me(klass);
		// Primitive Type: set it to all PlugPoints
		if (mirror.isStringLike() || mirror.isBoolean() || mirror.isNumber() || mirror.isChar())
		{
			this.setAll(obj);
		}
		// Map: set by key
		else if (mirror.isOf(Map.class))
		{
			Map<?, ?> map = (Map<?, ?>) obj;
			while (it.hasNext())
			{
				String key = it.next();
				try
				{
					this.set(key, map.get(key));
				}
				catch (Exception e)
				{
					this.set(key, "");
				}
			}
		}
		// POJO: set by field
		else
		{
			while (it.hasNext())
			{
				String key = it.next();
				try
				{
					this.set(key, mirror.getValue(obj, key));
				}
				catch (Exception e)
				{
					this.set(key, "");
				}
			}
		}
		return this;
	}

	public ISegment set(String key, Object v)
	{
		this.context.set(key, v);
		return this;
	}

	static class Node implements Cloneable
	{

		boolean isKey;

		String value;

		static Node key(String val)
		{
			Node node = new Node();
			node.isKey = true;
			node.value = val;
			return node;
		}

		static Node val(String val)
		{
			Node node = new Node();
			node.isKey = false;
			node.value = val;
			return node;
		}

		@Override
		public Node clone() throws CloneNotSupportedException
		{
			Node node = new Node();
			node.isKey = this.isKey;
			node.value = this.value;
			return node;
		}

	}

	private Context context;

	private List<Node> nodes;

	private NutMap keys;

	public void parse(Reader reader)
	{
		this.nodes = new LinkedList<Node>();
		this.context = new Context();
		this.keys = new NutMap();
		StringBuilder org = new StringBuilder();
		StringBuilder sb = new StringBuilder();
		int b;
		try
		{
			while (-1 != (b = reader.read()))
			{
				org.append((char) b);
				switch (b)
				{
					case '$':
						b = reader.read();
						org.append((char) b);
						// Escape
						if (b == '$')
						{
							sb.append((char) b);
						}
						// In Plug Point
						else if (b == '{')
						{
							// Save before
							if (sb.length() > 0)
							{
								this.nodes.add(Node.val(sb.toString()));
								sb = new StringBuilder();
							}
							// Search the end
							while (-1 != (b = reader.read()))
							{
								org.append((char) b);
								if (b == '}')
								{
									break;
								}
								sb.append((char) b);
							}
							if (b != '}')
							{
								throw Util.makeThrow("Error format around '%s'", sb);
							}
							// Create Key
							String key = sb.toString();
							this.nodes.add(Node.key(key));
							this.keys.put(key, null);
							sb = new StringBuilder();
						}
						// Normal
						else
						{
							sb.append('$').append((char) b);
						}
						break;
					default:
						sb.append((char) b);
				}
			}
			if (sb.length() > 0)
			{
				this.nodes.add(Node.val(sb.toString()));
			}
			// Store the Oraginal Value
			this.orgString = org.toString();
		}
		catch (IOException e)
		{
			throw Util.wrapThrow(e);
		}
	}

	public ISegment valueOf(String str)
	{
		this.parse(new StringReader(str));
		return this;
	}

	public CharSequence render()
	{
		return this.render(this.context);
	}

	public CharSequence render(Context context)
	{
		StringBuilder sb = new StringBuilder();
		for (Node node : this.nodes)
		{
			Object val = node.isKey ? context.get(node.value) : node.value;
			if (null == val)
			{
				continue;
			}
			if (val instanceof Collection<?>)
			{
				for (Object obj : (Collection<?>) val)
				{
					sb.append(obj);
				}
			}
			else
			{
				sb.append(val);
			}
		}
		return sb;
	}

	public Context getContext()
	{
		return this.context;
	}

	public void fillNulls(Context context)
	{
		for (String key : this.keys.keySet())
		{
			Object val = context.get(key);
			if (null == val)
			{
				context.set(key, "${" + key + "}");
			}
		}
	}

	@Override
	public String toString()
	{
		return this.render().toString();
	}

}
